﻿using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Media.Media3D;
using System.Windows.Threading;
using HelixToolkit;
using Microsoft.Win32;
using OpenSourceControls;

namespace Glissando
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// Using Leslie Sanford's MIDI Toolkit 
    /// http://www.lesliesanford.com/Programming/MIDIToolkit.shtml
    /// 
    /// MIDIs
    /// http://www.piano-midi.de/midicoll.htm
    /// http://members.shaw.ca/smythe/archive.htm (piano rolls)
    /// http://musicforpianos.com/piano%20roll%20midis.htm
    /// http://midistudio.com/midi/MidFram.htm
    /// http://www.kunstderfuge.com/
    /// 
    /// http://www.hum.aau.dk/~bovbjerg/piano4.html
    /// 
    /// Instruments
    /// http://en.wikipedia.org/wiki/General_MIDI 
    /// </summary>
    // Todo
    // - improve light positioning
    // - bevel edges on keys (3d)
    // - relaycommands
    // - move more logic from view to ViewModel
    public partial class MainWindow : Window
    {
        private const string statusUrl = @"http://www.objo.net/CodePlex/Download.aspx?q=status&app=Glissando";
        private const string updateUrl = @"http://www.objo.net/CodePlex/Download.aspx?q=latest&app=Glissando";

        public static RoutedUICommand ChangeInstrument = new RoutedUICommand("_Change instrument", "ChangeInstrument",
                                                                             typeof (MainWindow));

        public static RoutedUICommand ColoredKeys = new RoutedUICommand("_Colored keys\tF9", "ColoredKeys",
                                                                        typeof (MainWindow),
                                                                        new InputGestureCollection(new InputGesture[]
                                                                                                       {
                                                                                                           new KeyGesture
                                                                                                               (Key.F9)
                                                                                                       }));

        // todo - replace with RelayCommands

        public static RoutedUICommand ExitApplication = new RoutedUICommand("E_xit",
                                                                            "ExitApplication", typeof (MainWindow),
                                                                            new InputGestureCollection(
                                                                                new InputGesture[]
                                                                                    {
                                                                                        new KeyGesture(Key.F12,
                                                                                                       ModifierKeys.
                                                                                                           Control)
                                                                                    }));

        public static RoutedUICommand ExitViewFullScreen = new RoutedUICommand("_Exit full screen\tEsc",
                                                                               "ExitViewFullScreen", typeof (MainWindow),
                                                                               new InputGestureCollection(
                                                                                   new InputGesture[]
                                                                                       {new KeyGesture(Key.Escape)}));

        public static RoutedUICommand HelpAbout = new RoutedUICommand("_About",
                                                                      "HelpAbout", typeof (MainWindow),
                                                                      new InputGestureCollection(new InputGesture[]
                                                                                                     {
                                                                                                         new KeyGesture(
                                                                                                             Key.F10)
                                                                                                     }));

        public static RoutedUICommand NormalSpeed = new RoutedUICommand("_Normal Speed\tCtrl-N", "NormalSpeed",
                                                                        typeof (MainWindow),
                                                                        new InputGestureCollection(new InputGesture[]
                                                                                                       {
                                                                                                           new KeyGesture
                                                                                                               (Key.Add)
                                                                                                       }));

        public static RoutedUICommand Pause = new RoutedUICommand("_Pause", "Pause", typeof (MainWindow));

        public static RoutedUICommand ReduceSpeed = new RoutedUICommand("_Reduce Speed\tCtrl--", "ReduceSpeed",
                                                                        typeof (MainWindow),
                                                                        new InputGestureCollection(new InputGesture[]
                                                                                                       {
                                                                                                           new KeyGesture
                                                                                                               (Key.
                                                                                                                    Subtract)
                                                                                                       }));

        public static RoutedUICommand SetLoopEnd = new RoutedUICommand("Loop end", "LoopEnd", typeof (MainWindow),
                                                                       new InputGestureCollection(new InputGesture[]
                                                                                                      {
                                                                                                          new KeyGesture
                                                                                                              (Key.
                                                                                                                   NumPad2)
                                                                                                      }));

        public static RoutedUICommand SetLoopStart = new RoutedUICommand("Loop start", "LoopStart", typeof (MainWindow),
                                                                         new InputGestureCollection(new InputGesture[]
                                                                                                        {
                                                                                                            new KeyGesture
                                                                                                                (Key.
                                                                                                                     NumPad1)
                                                                                                        }));

        public static RoutedUICommand Settings = new RoutedUICommand("_Settings...", "Settings", typeof (MainWindow),
                                                                     new InputGestureCollection(new InputGesture[]
                                                                                                    {
                                                                                                        new KeyGesture(
                                                                                                            Key.F10)
                                                                                                    }));

        public static RoutedUICommand Start = new RoutedUICommand("_Start", "Start", typeof (MainWindow));

        public static RoutedUICommand StartPitchTest = new RoutedUICommand("Pitch/chord recognition _test\tF5",
                                                                           "StartPitchTest", typeof (MainWindow),
                                                                           new InputGestureCollection(
                                                                               new InputGesture[]
                                                                                   {new KeyGesture(Key.F5)}));

        public static RoutedUICommand StartStop = new RoutedUICommand("_Start/Stop", "StartStop", typeof (MainWindow),
                                                                      new InputGestureCollection(new InputGesture[]
                                                                                                     {
                                                                                                         new KeyGesture(
                                                                                                             Key.Space)
                                                                                                     }));

        public static RoutedUICommand Stop = new RoutedUICommand("_Stop", "Stop", typeof (MainWindow));
        public static RoutedUICommand ViewExport = new RoutedUICommand("_Export...", "ViewExport", typeof (MainWindow));

        public static RoutedUICommand ViewFullScreen = new RoutedUICommand("_Full screen\tF11",
                                                                           "ViewFullScreen", typeof (MainWindow),
                                                                           new InputGestureCollection(
                                                                               new InputGesture[]
                                                                                   {new KeyGesture(Key.F11)}));

        public static RoutedUICommand ViewStereo = new RoutedUICommand("_Stereo\t\tF8",
                                                                       "ViewStereo", typeof (MainWindow),
                                                                       new InputGestureCollection(new InputGesture[]
                                                                                                      {
                                                                                                          new KeyGesture
                                                                                                              (Key.F8)
                                                                                                      }));


        private readonly MainViewModel model;
        private readonly DispatcherTimer timer;

        public MainWindow()
        {
            InitializeComponent();
            CheckForUpdate();

            AddCommandBindings();

            // Add a handler for pressing the keys on the piano with the mouse
            View.MouseLeftButtonDown += Piano_MouseLeftButtonDown;
            View.MouseMove += Piano_MouseMove;
            View.MouseLeftButtonUp += Piano_MouseLeftButtonUp;

            model = new MainViewModel(this, piano, null);
            DataContext = model;

            ParseCommandLineArguments();

            // todo - not neccessary to use a timer to check position of the sequence
            timer = new DispatcherTimer {Interval = new TimeSpan(0, 0, 0, 0, 10), IsEnabled = true};
            timer.Tick += timer_Tick;
            Loaded += MainWindow_Loaded;
        }

        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            View.LookAt(piano.GetKeyPosition(60), new Vector3D(302, 1170, -1240), 0);
        }

        private void CheckForUpdate()
        {
            // StatusDialog.ShowIfUpdated(Properties.Settings.Default, statusUrl);
            AutoUpdateDialog.ShowIfUpdateAvailable(Properties.Settings.Default, updateUrl);
        }

        private void ParseCommandLineArguments()
        {
            // Accept midi file as program startup argument
            string[] args = Environment.GetCommandLineArgs();
            for (int i = 1; i < args.Length; i++)
            {
                string arg = args[i].Trim();
                if (arg.ToLower() == "/t")
                {
                    SetTransparentMode();
                }
                if (File.Exists(arg) && Path.GetExtension(arg).ToLower() == ".mid")
                    OpenFile(arg);
            }
        }

        private void SetTransparentMode()
        {
            AllowsTransparency = true;
            Background = Brushes.Transparent;
            WindowStyle = WindowStyle.None;
            MainGrid.Background = Brushes.Transparent;
        }

        private void timer_Tick(object sender, EventArgs e)
        {
            model.CheckLoop();
        }

        public void SetWallPaper()
        {
            MainGrid.Background = new ImageBrush(new BitmapImage(new Uri(@"wallpaper.jpg")));
        }

        private void AddCommandBindings()
        {
            CommandBindings.Add(new CommandBinding(ApplicationCommands.Open, FileOpenHandler));
            CommandBindings.Add(new CommandBinding(StartPitchTest, StartPitchTestHandler));
            CommandBindings.Add(new CommandBinding(ExitApplication, FileExitHandler));
            CommandBindings.Add(new CommandBinding(HelpAbout, HelpAboutHandler));

            CommandBindings.Add(new CommandBinding(Settings, SettingsHandler));

            CommandBindings.Add(new CommandBinding(ViewExport, ViewExportHandler));
            CommandBindings.Add(new CommandBinding(ViewFullScreen, ViewFullScreenHandler));
            CommandBindings.Add(new CommandBinding(ExitViewFullScreen, ExitViewFullScreenHandler));
            CommandBindings.Add(new CommandBinding(ViewStereo, ViewStereoHandler));
            CommandBindings.Add(new CommandBinding(ColoredKeys, ColoredKeysHandler));

            CommandBindings.Add(new CommandBinding(Start, StartHandler, CanStartHandler));
            CommandBindings.Add(new CommandBinding(Stop, StopHandler, CanStopHandler));
            CommandBindings.Add(new CommandBinding(StartStop, StartStopHandler, CanStartHandler));
            CommandBindings.Add(new CommandBinding(Pause, PauseHandler, CanPauseHandler));
            CommandBindings.Add(new CommandBinding(ReduceSpeed, ReduceSpeedHandler, CanPauseHandler));
            CommandBindings.Add(new CommandBinding(NormalSpeed, NormalSpeedHandler, CanPauseHandler));
            CommandBindings.Add(new CommandBinding(SetLoopStart, SetLoopStartHandler, CanLoopHandler));
            CommandBindings.Add(new CommandBinding(SetLoopEnd, SetLoopEndHandler, CanLoopHandler));
        }

        private void OpenFile(string path)
        {
            model.Open(path);
        }

        protected override void OnClosing(CancelEventArgs e)
        {
            base.OnClosing(e);
            model.IsClosing = true;
        }

        protected override void OnClosed(EventArgs e)
        {
            base.OnClosed(e);
            model.Close();
        }

        // todo - better check/uncheck of the selected instrument - binding?
        private void ChangeInstrument_Click(object sender, RoutedEventArgs e)
        {
            foreach (MenuItem m in InstrumentsMenu.Items)
                m.IsChecked = false;

            var mi = sender as MenuItem;
            int instrument = int.Parse(mi.Tag.ToString());
            model.SetInstrument(1, instrument);
            mi.IsChecked = true;
        }

        private void HelpWeb_Click(object sender, RoutedEventArgs e)
        {
            Process.Start("http://glissando.codeplex.com");
        }

        #region Handle piano mouse events

        private int currentKeyNumber;

        private void Piano_MouseMove(object sender, MouseEventArgs e)
        {
            if (e.LeftButton != MouseButtonState.Pressed)
                return;
            int keyNumber = getKeyNumber(e);
            if (keyNumber != currentKeyNumber)
            {
                model.Release(currentKeyNumber,true);
                if (keyNumber > 0)
                {
                    model.Play(keyNumber, true);
                    currentKeyNumber = keyNumber;
                }
            }
        }

        private int getKeyNumber(MouseEventArgs e)
        {
            int note = 0;
            Point mousePos = e.GetPosition(View);
            var hitParams = new PointHitTestParameters(mousePos);
            VisualTreeHelper.HitTest(View, null, delegate(HitTestResult hr)
                                                     {
                                                         var rayHit = hr as RayMeshGeometry3DHitTestResult;
                                                         if (rayHit != null)
                                                         {
                                                             note =
                                                                 piano.GetKeyNumber(rayHit.ModelHit as GeometryModel3D);
                                                             return HitTestResultBehavior.Stop;
                                                         }
                                                         return HitTestResultBehavior.Continue;
                                                     }, hitParams);
            return note;
        }

        private void Piano_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            model.Release(currentKeyNumber,true);
            currentKeyNumber = 0;
        }

        private void Piano_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            int note = getKeyNumber(e);
            model.Release(currentKeyNumber,true);
            model.Play(note,true);
            currentKeyNumber = note;
        }

        #endregion

        #region Handle keyboard events

        private void Window_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.IsRepeat)
                return;
            int note = noteFromKey(e.Key);
            if (note > 0)
            {
                model.Play(note,true);
                model.PitchTestCheckInput();
            }
        }

        private void Window_KeyUp(object sender, KeyEventArgs e)
        {
            if (e.IsRepeat)
                return;
            int note = noteFromKey(e.Key);
            if (note > 0)
            {
                model.Release(note,true);
            }
        }

        // now matching 
        // http://www.charlespetzold.com/blog/2009/07/A-Simple-Silverlight-3-Synthesizer-with-Keyboard-of-Sorts.html
        private int noteFromKey(Key key)
        {
            int a = 60;
            if (Keyboard.IsKeyDown(Key.LeftCtrl)) a -= 12;
            if (Keyboard.IsKeyDown(Key.LeftShift)) a += 12;

            int c = a - 12;

            switch (key)
            {
                case Key.Q:
                    return a + 0;
                case Key.W:
                    return a + 2;
                case Key.E:
                    return a + 4;
                case Key.R:
                    return a + 5;
                case Key.T:
                    return a + 7;
                case Key.Y:
                    return a + 9;
                case Key.U:
                    return a + 11;
                case Key.I:
                    return a + 12;
                case Key.O:
                    return a + 14;
                case Key.P:
                    return a + 16;

                case Key.D2:
                    return a + 1;
                case Key.D3:
                    return a + 3;
                case Key.D5:
                    return a + 6;
                case Key.D6:
                    return a + 8;
                case Key.D7:
                    return a + 10;
                case Key.D9:
                    return a + 13;
                case Key.D0:
                    return a + 15;

                case Key.Z:
                    return c + 0;
                case Key.S:
                    return c + 1;
                case Key.X:
                    return c + 2;
                case Key.D:
                    return c + 3;
                case Key.C:
                    return c + 4;
                case Key.V:
                    return c + 5;
                case Key.G:
                    return c + 6;
                case Key.B:
                    return c + 7;
                case Key.H:
                    return c + 8;
                case Key.N:
                    return c + 9;
                case Key.J:
                    return c + 10;
                case Key.M:
                    return c + 11;
            }
            return 0;
        }

        #endregion

        #region command handlers

        private bool IsFullScreen
        {
            get { return model.IsFullScreen; }
            set
            {
                model.IsFullScreen = value;
                WindowStyle = value || AllowsTransparency ? WindowStyle.None : WindowStyle.ThreeDBorderWindow;
                WindowState = value ? WindowState.Maximized : WindowState.Normal;
                MainMenu.Visibility = value ? Visibility.Collapsed : Visibility.Visible;
            }
        }

        private void FileOpenHandler(object sender, ExecutedRoutedEventArgs e)
        {
            model.Stop();
            var d = new OpenFileDialog();
            d.DefaultExt = ".mid";
            d.FileName = "*.mid";
            d.Filter = "Midi files (*.mid)|*.mid";
            if (d.ShowDialog().Value)
            {
                OpenFile(d.FileName);
            }
        }

        private void StartPitchTestHandler(object sender, ExecutedRoutedEventArgs e)
        {
            model.IsPitchTestRunning = !model.IsPitchTestRunning;
        }

        private void SettingsHandler(object sender, ExecutedRoutedEventArgs e)
        {
            var d = new SettingsDialog();
            d.DataContext = model;
            if (d.ShowDialog().Value)
            {
                model.ReconnectDevices();
                model.UpdatePitchTestSettings();
            }
        }

        private void FileExitHandler(object sender, ExecutedRoutedEventArgs e)
        {
            Close();
        }

        private void ViewExportHandler(object sender, RoutedEventArgs e)
        {
            var d = new SaveFileDialog();
            d.Filter = "Kerkythea files (*.xml)|*.xml";
            d.DefaultExt = ".xml";
            d.FileName = "*.xml";
            if (!d.ShowDialog().Value)
                return;

            var exporter = new KerkytheaExporter(d.FileName);
            exporter.BackgroundColor = (MainGrid.Background as SolidColorBrush).Color;
            exporter.Reflections = true;
            exporter.Shadows = true;
            exporter.SoftShadows = true;
            exporter.Width = (int) View.ActualWidth;
            exporter.Height = (int) View.ActualHeight;
            exporter.Export(View.Viewport);
            exporter.Close();
        }

        private void ViewFullScreenHandler(object sender, RoutedEventArgs e)
        {
            IsFullScreen = !IsFullScreen;
        }

        private void ViewStereoHandler(object sender, RoutedEventArgs e)
        {
            model.Stereo = !model.Stereo;
        }

        private void ExitViewFullScreenHandler(object sender, RoutedEventArgs e)
        {
            IsFullScreen = false;
        }

        private string GetApplicationTitle()
        {
            Assembly asm = Assembly.GetExecutingAssembly();
            Version version = asm.GetName().Version;
            FileVersionInfo fvi = FileVersionInfo.GetVersionInfo(asm.Location);
            return String.Format("{0} {1}.{2}", fvi.ProductName, version.Major, version.Minor);
        }

        private void HelpAboutHandler(object sender, ExecutedRoutedEventArgs e)
        {
            MessageBox.Show("http://glissando.codeplex.com", GetApplicationTitle(), MessageBoxButton.OK,
                            MessageBoxImage.Information, MessageBoxResult.OK);
        }

        private void ColoredKeysHandler(object sender, RoutedEventArgs e)
        {
            model.ColoredKeys = !model.ColoredKeys;
            model.ReleaseAllKeys();
            //piano.ReleaseAllKeys();
        }

        private void StartHandler(object sender, RoutedEventArgs e)
        {
            model.Start();
        }

        private void CanStartHandler(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = model.CanPlay;
        }

        private void StartStopHandler(object sender, RoutedEventArgs e)
        {
            // todo - stop not working
            if (model.IsPlaying)
                model.Pause();
            else
                model.Start();
            Focus();
        }

        private void StopHandler(object sender, RoutedEventArgs e)
        {
            model.Stop();
        }

        private void CanStopHandler(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = model.IsPlaying;
        }

        private void PauseHandler(object sender, RoutedEventArgs e)
        {
            model.Pause();
        }

        private void CanPauseHandler(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = model.IsPlaying;
        }

        private void ReduceSpeedHandler(object sender, RoutedEventArgs e)
        {
            model.ReduceSpeed();
        }

        private void NormalSpeedHandler(object sender, RoutedEventArgs e)
        {
            model.NormalSpeed();
        }

        private void CanLoopHandler(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = model.IsMidiFileLoaded;
        }

        private void SetLoopStartHandler(object sender, RoutedEventArgs e)
        {
            model.SetLoopStart();
        }

        private void SetLoopEndHandler(object sender, RoutedEventArgs e)
        {
            model.SetLoopEnd();
        }

        #endregion

        #region drag-drop

        private void Window_Drop(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop))
            {
                var fileNames = e.Data.GetData(DataFormats.FileDrop, true) as string[];
                foreach (string filename in fileNames)
                {
                    OpenFile(filename);
                }
            }

            e.Handled = true;
        }

        #endregion

        private void HelpForum_Click(object sender, RoutedEventArgs e)
        {
            Process.Start("http://glissando.codeplex.com/Thread/List.aspx");
        }
        private void Feedback_Click(object sender, RoutedEventArgs e)
        {
            MessageBox.Show("Send your feedback to glissando@objo.net", GetApplicationTitle(), MessageBoxButton.OK,
                            MessageBoxImage.Information, MessageBoxResult.OK);
//             Process.Start("mailto:glissando@objo.net");
        }
    }
}